package it.avantsoft.ls.peer;

import it.avantsoft.ls.base.Application;
import it.avantsoft.ls.base.Configuration;
import it.avantsoft.ls.gl.GroupChildHandler;
import it.avantsoft.ls.gl.GroupLeaderHandler;
import it.avantsoft.ls.gl.data.AddressBook;
import it.avantsoft.ls.gl.data.AddressBookManager;
import it.avantsoft.ls.peer.command.PeerCommand;
import it.avantsoft.ls.util.SimpleEntry;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.net.Socket;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;

public class PeerHandler extends Thread {
    private static Logger log = Logger.getLogger(PeerHandler.class);
    private boolean connected = false;
    private Map.Entry<String, Integer> groupLeader;
    private SimpleEntry<String, Integer> groupLeaderConnecting;
    private String groupLeaderId;

    public boolean addCentralPeer(String id, int port) {
        return addressBookManager.addCentralPeer(id, port);
    }

    public boolean addAllowedPeer(String id) {
        return addressBookManager.addAllowedPeer(id);
    }

    public void addUpperPeer(String id) {
        addressBookManager.addUpperPeer(id);
    }

    public boolean deadNiece(String peerID) {
        return groupLeaderHandler.removeAllowedPeer(peerID);
    }

    public boolean deadBrother(String peerID) {
        return groupLeaderHandler.removeBrother(peerID);
    }

    public boolean deadUncle(String peerID) {
        return groupLeaderHandler.removeUncle(peerID);
    }

    public boolean signalDeadUncle(String peerID) {
        boolean signaled = true;
        for (GroupChildHandler hand : groupLeaderHandler.getLabHandlers()) {
            signaled &= hand.signalDeadUncle(peerID);
        }
        return signaled;
    }

    public boolean isConnected() {
        return connected;
    }

    public boolean sendHorizontalExchangeablePeer(SimpleEntry worstPeer, String designedGroupLeaderId) {
        return senderMessage.signalHorizontalExchangeablePeer(worstPeer, designedGroupLeaderId);
    }

    public void executeHorizontalBalanceCommand(String exchangeablePeer, String designedGroupLeaderHost, int designedGroupLeaderListeningPort) {
        groupLeaderHandler.executeHorizontalBalanceCommand(exchangeablePeer, designedGroupLeaderHost, designedGroupLeaderListeningPort);
    }

    public enum PeerState {
        DEAD,
        NOT_INITIALIZED,
        INITIALIZED,
        CONNECTING,
        CONNECTED,
        HORIZONTAL_BALANCE,
        VERTICAL_BALANCE,
        GROUPLEADER_DEATH
    }

    private PeerState state = PeerState.NOT_INITIALIZED;
    private int number;
    private int level;
    private String peerId;
    private Socket socket;
    private SenderMessageToGL senderMessage;
    private ReceiverCommandFromGL receiverCommand;
    private LinkedBlockingQueue<PeerCommand> commandQueue;
    private GroupLeaderHandler groupLeaderHandler;
    private AddressBookManager addressBookManager;


    public PeerHandler(Application application, int number, AddressBookManager addressBookManager) {
        super("PeerHandler " + number);
        this.number = number;
        groupLeaderHandler = application.getGroupLeaderHandler();
        this.addressBookManager = addressBookManager;
        start(Configuration.SERVER_HOST, Configuration.SERVER_TCP_PORT);
    }

    public int getNumber() {
        return number;
    }

    public void setPeerId(String peerId) {
        if (this.peerId == null) {
            setName(getName() + " - " + peerId);
            receiverCommand.setName(receiverCommand.getName() + peerId);
            this.peerId = peerId;
            groupLeaderHandler.setSelfInfo(peerId, groupLeaderHandler.getListeningPort());
        } else {
            if (!this.peerId.equals(peerId)) {
                log.error("Different PeerId - old:" + this.peerId + " new:" + peerId);
            }
        }
    }

    public String getPeerId() {
        return peerId;
    }

    public void setGroupLeaderId(String groupLeaderId) {
        this.groupLeaderId = groupLeaderId;
    }

    public String toString() {
        return "PeerHandler{" +
                number +
                ", state=" + state +
                ", gl=" + groupLeaderId +
                '}';
    }

    ////////////////////////////////////////////////

    private void start(String host, int port) {
        try {
            socket = new Socket(host, port);

            senderMessage = new SenderMessageToGL(socket.getOutputStream());

            commandQueue = new LinkedBlockingQueue<PeerCommand>();

            receiverCommand = new ReceiverCommandFromGL(this, socket.getInputStream(), commandQueue);
            receiverCommand.start();

            changeState(PeerState.INITIALIZED);
        } catch (IOException e) {
            log.error("peer starting connection with " + host + ":" + port, e);
        }
    }

    public void shutdown() {
        try {
            socket.close();
        } catch (IOException e) {

        } finally {
            log.info("socket closed");
            if (state.equals(PeerState.CONNECTED)) {
                changeState(PeerState.DEAD);
                interrupt();
            }
        }
    }

    private void changeState(PeerState state) {
        //log.info("state = " + state);
        this.state = state;
        connected = state.equals(PeerState.CONNECTED);
    }

    private PeerCommand getCommadFromQueue() {
        PeerCommand peerCommand = null;
        try {
            peerCommand = commandQueue.take();
            log.info("receive " + peerCommand);
        } catch (InterruptedException e) {
            log.error(e);  //To change body of catch statement use File | Settings | File Templates.
        }
        return peerCommand;
    }

    public void run() {
        while (!state.equals(PeerState.DEAD)) {
            switch (state) {
                case INITIALIZED:
                    sendWelcome();
                    break;
                case CONNECTING: //può ricevere AB, ConnectToGL
                case CONNECTED: //può ricevere NewChild, HorizontalBalance, Ping -> Pong, PeerAlive -> AckPeerAlive
                    PeerCommand commadFromQueue = getCommadFromQueue();
                    if (commadFromQueue != null) {
                        commadFromQueue.execute();
                    }
                    break;
                case GROUPLEADER_DEATH:
                    Map.Entry<String, Integer> glHost = addressBookManager.getNextGL();
                    //TODO host
                    connectToGL("127.0.0.1", glHost.getValue(), true);
                    break;

                case HORIZONTAL_BALANCE:
                    if (horizontalBalance()) {
                        changeState(PeerState.CONNECTED);
                    }
                    break;
                case VERTICAL_BALANCE:
                    if (verticalBalance()) {
                        changeState(PeerState.CONNECTED);
                    }
                    break;
            }
        }
    }

    public void connectToGL(String groupLeaderHost, int groupLeaderListeningPort) {
        connectToGL(groupLeaderHost, groupLeaderListeningPort, false);
    }

    public void connectToGL(String groupLeaderHost, int groupLeaderListeningPort, boolean reconnect) {
        shutdown();

        start(groupLeaderHost, groupLeaderListeningPort);

        groupLeaderConnecting = new SimpleEntry<String, Integer>(groupLeaderHost, groupLeaderListeningPort);
        if (reconnect) {
            log.info("reconnecting... " + this);
            sendReconnect(peerId, groupLeaderId);
        } else {
            log.info("connecting... " + this);
            sendConnect(peerId);
        }
    }

    public boolean newAB(AddressBook addressBook) {

        boolean success = addressBookManager.newAB(addressBook);
        if (groupLeader != groupLeaderConnecting) {
            groupLeader = groupLeaderConnecting;
        }
        if (success) {
            setLevel(addressBookManager.getLevel());
            changeState(PeerState.CONNECTED);
        }
        return success;
    }

    public boolean verticalBalance() {
        changeState(PeerState.VERTICAL_BALANCE);
        //TODO verticalBalance
        return true;
    }

    public boolean horizontalBalance() {
        changeState(PeerState.HORIZONTAL_BALANCE);
        //TODO horizontalBalance
        return true;
    }

    public boolean horizontalBalance(int loadDifference, String designedGroupLeaderId) {

        return groupLeaderHandler.horizontalBalance(loadDifference, designedGroupLeaderId);


    }
    //////////////////////////////////////////////////////

    public void addChild(String childId) {
        if (groupLeaderHandler.addIncomingAllowedChild(childId)) {
            String leaderHost = groupLeaderHandler.getHostAddress();
            int leaderPort = groupLeaderHandler.getListeningPort();
            sendAckNewChild(childId, leaderHost, leaderPort);
        } else {
            groupLeaderHandler.newChildtoBestPeer(childId);
        }
    }

    /////////////////////////////////////////////////////

    public boolean sendWelcome() {
        boolean sent = senderMessage.welcome(groupLeaderHandler.getListeningPort());
        if (sent) {
            changeState(PeerState.CONNECTING);
        }
        return sent;
    }

    private boolean sendConnect(String peerId) {
        boolean sent = senderMessage.connect(peerId, groupLeaderHandler.getListeningPort());
        if (sent) {
            changeState(PeerState.CONNECTING);
        }
        return sent;
    }

    /**
     * Chiamato dal Peer sull'aggiunta di un child
     * o dal GroupLeader sull'arrivo di un AckNewChild da girare al padre
     *
     * @param childId
     * @param groupLeaderHost
     * @param groupLeaderPort
     * @return
     */
    public boolean sendAckNewChild(String childId, String groupLeaderHost, int groupLeaderPort) {
        return senderMessage.ackNewChild(childId, groupLeaderHost, groupLeaderPort);
    }

    public boolean sendReconnect(String peerId, String exGroupLeaderId) {
        boolean sent = senderMessage.reconnect(peerId, exGroupLeaderId);
        if (sent) {
            changeState(PeerState.CONNECTING);
        }
        return sent;
    }

    public boolean sendAppraisals() {
        //TODO
        return true;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public int getLevel() {
        return level;
    }

    public boolean signalSon(String id) {
        return senderMessage.signalSon(id);
    }

    public boolean signalNewUncle(String id, int port) {
        return groupLeaderHandler.signalNewUncle(id, port);
    }

    public PeerState getPeerState() {
        return state;
    }

    public Map.Entry<String, Integer> getGroupLeader() {
        return groupLeader;
    }

    public void deadGL() {
        changeState(PeerState.GROUPLEADER_DEATH);
        interrupt();
    }
}
